/*
Continuous Testing Toolkit
Copyright (C) 2009 Paul R. Holser, Jr.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

package org.infinitest.toolkit.internal.reflection;

import static org.infinitest.toolkit.internal.reflection.ClassUtilities.*;

import java.lang.reflect.Constructor;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * <p>
 * This class is intended to be a replacement for {@link java.lang.Class#getMethod(String,Class[]) Class.getMethod} and
 * {@link java.lang.Class#getConstructor(Class[]) Class.getConstructor}.
 * </p>
 *
 * <p>
 * The motivation behind constructing such replacements is that sometimes you want to find a method with a given name
 * whose formal parameters are compatible with the runtime types of some actual parameter objects for a reflective
 * invocation. Our natural inclination is to build up a {@link Class} array consisting of the results of calls to
 * {@link Object#getClass() getClass} on the actual parameter objects, and to use that {@code Class[]} to call {@code
 * getMethod} on the class that has the method you want to invoke reflectively.
 * </p>
 *
 * <p>
 * However, {@code Class.getMethod} looks for <em>exact</em> matches in the signature. In many programming scenarios,
 * this is no big deal, since often you know the exact signature in advance. But in other scenarios (ORBs, scripting
 * languages), you want to find a <em>compatible</em> method for reflective invocation, not necessarily an exact
 * signature match. If you use <code>Class.getMethod</code> in such scenarios:
 * </p>
 *
 * <ol>
 * <li>You'll never find methods containing primitive formal parameters. The class of an actual parameter could be, say,
 * {@code java.lang.Integer}, but that won't match an {@code int} formal parameter. Even if this were resolved, you'd
 * still want to be able to find methods that could be invoked because of primitive widening (for example, you can pass
 * an {@code int} to a method that expects a {@code long}. No luck here either.</li>
 *
 * <li>You'll never find methods containing superclass or interface formal parameters using the {@code Class} of a
 * subclass or interface-implementor actual parameter. The ultimate types of those actual parameters are compatible with
 * the formal parameter types, but aren't exactly the same, so {@code getMethod} fails.</li>
 *
 * <li>Sometimes actual arguments can be {@code null}, intentionally. {@code null} is compatible with object references
 * and array references, but not with primitives. How can we signal our intent to {@code getMethod}?</li>
 *
 * <li>Even when {@code getMethod} finds a method, you still might not be able to invoke it. Consider this case:
 *
 * <pre>
 * public void mutuallyAmbiguous(List&lt;?&gt; list) {
 *     Iterator&lt;?&gt; i = list.iterator();
 *     Method m = i.getClass().getMethod(&quot;hasNext&quot;, new Class[0]);
 *     boolean b = ((Boolean) m.invoke(i, new Object[0])).booleanValue();
 * }
 * </pre>
 *
 * The problem here is that the true type of the object returned from {@code iterator} is an anonymous inner class,
 * which is inaccessible. {@code invoke} would therefore throw {@code IllegalAccessException}.</li>
 * </ol>
 *
 * <p>
 * So, you must be careful to avoid all these pitfalls. You also need to deal with ambiguity. I need to return a method
 * that is compatible for reflective invocation, but what if there's more than one? How do I choose which one to return?
 * Ideally, the solution would follow the same rules that a Java compiler does when resolving method calls in source.
 * How does a Java compiler handle things like this?
 * </p>
 *
 * <pre>
 * System.out.println(null);
 * </pre>
 *
 * <p>
 * It chooses the match with the narrowest type. So {@link java.io.PrintStream#println(String) println(String)} wins
 * over {@link java.io.PrintStream#println(Object) println(Object)}. Section 15.12.2 of the <a
 * href="http://java.sun.com/docs/books/jls/index.html">Java Language Specification</a> governs the choice of overloaded
 * methods based on the actual arguments--compilers are to choose the <em>most specific</em> method, as described
 * therein.
 * </p>
 *
 * <p>
 * This class and its extensions handle all of the above cases. It can find constructors using the same principles.
 * </p>
 *
 * <p>
 * Instances are constructed with a {@code Class} object that is to be the target of method and constructor queries. The
 * methods {@link #findMethod(String,Class[]) findMethod} and {@link #findConstructor(Class[]) findConstructor} perform
 * the queries. The different signatures of {@code getParameterTypesFrom} provide a convenient way to map from {@code
 * Object}s or {@code String}s containing type names to {@code Class} arrays suitable for passing to {@code findMethod}
 * or {@code findConstructor}.
 * </p>
 *
 * <p>
 * Example:
 * </p>
 *
 * <pre>
 * Byte b1 = new Byte(&quot;2&quot;);
 * Byte b2 = new Byte(&quot;5&quot;);
 * Object[] args = new Object[] { b1, b2 };
 * MethodFinder&lt;Math&gt; f = methodFinderFor(Math.class);
 * Method m = f.findMethod(&quot;max&quot;, f.getParameterTypesFrom(args));
 * </pre>
 *
 * <p>
 * The found method corresponds to {@code Math.max(int,int)}, returning {@code int}, since it is the most specific
 * overloading of {@code Math.max} that has formal parameters compatible with {@code byte}s.
 *
 * @param <T> the type of the class on which methods and constructors will be found
 * @author <a href="http://www.pholser.com">Paul Holser</a>
 */
public class MethodFinder<T> {
    private final Class<T> clazz;
    private final Map<String, List<Method>> methodsByName = new HashMap<String, List<Method>>();
    private final List<Constructor<T>> ctors = new ArrayList<Constructor<T>>();
    private final Map<Member, Class<?>[]> memberParameters = new HashMap<Member, Class<?>[]>();

    public static <U> MethodFinder<U> methodFinderFor(Class<U> clazz) {
        return new MethodFinder<U>(clazz);
    }

    private MethodFinder(Class<T> clazz) {
        if (clazz == null)
            throw new IllegalArgumentException("null Class parameter");

        if (clazz.isPrimitive())
            throw new IllegalArgumentException("primitive Class parameter");

        if (clazz.isArray())
            throw new IllegalArgumentException("array Class parameter");

        this.clazz = clazz;
        loadMethods();
        loadConstructors();
    }

    public Constructor<T> findConstructor(Class<?>[] parameterTypes) throws NoSuchMethodException {
        Class<?>[] types = parameterTypes;
        if (types == null)
            types = new Class<?>[0];

        return findMemberIn(ctors, types);
    }

    public Method findMethod(String methodName, Class<?>[] parameterTypes) throws NoSuchMethodException {
        List<Method> methods = methodsByName.get(methodName);

        if (methods == null)
            throw new NoSuchMethodException("no method named " + clazz.getName() + '.' + methodName);

        Class<?>[] types = parameterTypes;
        if (types == null)
            types = new Class<?>[0];

        return findMemberIn(methods, types);
    }

    public Class<?>[] getParameterTypesFrom(Object[] args) {
        Class<?>[] argTypes = null;

        if (args != null) {
            argTypes = new Class[args.length];

            for (int i = 0; i < args.length; ++i)
                argTypes[i] = args[i] == null ? Void.TYPE : args[i].getClass();
        }
        else
            argTypes = new Class<?>[0];

        return argTypes;
    }

    public Class<?>[] getParameterTypesFrom(String[] names) throws ClassNotFoundException {
        return getParameterTypesFrom(names, getClass().getClassLoader());
    }

    public Class<?>[] getParameterTypesFrom(String[] names, ClassLoader loader) throws ClassNotFoundException {
        Class<?>[] types;

        if (names != null) {
            types = new Class[names.length];

            for (int i = 0; i < names.length; ++i)
                types[i] = classForNameOrPrimitive(names[i], loader);
        }
        else
            types = new Class<?>[0];

        return types;
    }

    private <M extends Member> M findMemberIn(List<M> candidates, Class<?>[] parameterTypes)
        throws NoSuchMethodException {

        List<M> matches = new ArrayList<M>();

        for (M each : candidates) {
            Class<?>[] methodParamTypes = memberParameters.get(each);

            if (Arrays.equals(methodParamTypes, parameterTypes))
                return each;

            if (compatibleClasses(methodParamTypes, parameterTypes))
                matches.add(each);
        }

        if (matches.isEmpty()) {
            throw new NoSuchMethodException("no member in " + clazz + " matching given args");
        }

        return matches.size() == 1 ? matches.get(0) : findMostSpecificMemberIn(matches);
    }

    private <M extends Member> M findMostSpecificMemberIn(List<M> candidates) throws NoSuchMethodException {
        List<M> mostSpecificMembers = new ArrayList<M>();

        for (M member : candidates) {
            if (mostSpecificMembers.isEmpty()) {
                // First guy in is the most specific so far.
                mostSpecificMembers.add(member);
            }
            else {
                boolean moreSpecific = true;
                boolean lessSpecific = false;

                // Is member more specific than everyone in the most-specific set?
                for (Member moreSpecificMember : mostSpecificMembers) {
                    if (!memberIsMoreSpecific(member, moreSpecificMember)) {
                        /*
                         * Can't be more specific than the whole set. Bail out, and mark whether member is less specific
                         * than the member under consideration. If it is less specific, it need not be added to the
                         * ambiguity set. This is no guarantee of not getting added to the ambiguity set...we're just
                         * not clever enough yet to make that assessment.
                         */

                        moreSpecific = false;
                        lessSpecific = memberIsMoreSpecific(moreSpecificMember, member);
                        break;
                    }
                }

                if (moreSpecific) {
                    // Member is the most specific now.
                    mostSpecificMembers.clear();
                    mostSpecificMembers.add(member);
                }
                else if (!lessSpecific) {
                    // Add to ambiguity set if mutually un-specific.
                    mostSpecificMembers.add(member);
                }
            }
        }

        if (mostSpecificMembers.size() > 1) {
            throw new NoSuchMethodException("Ambiguous request for member in " + clazz.getName()
                + " matching given args");
        }

        return mostSpecificMembers.get(0);
    }

    private void loadConstructors() {
        for (Constructor<T> each : clazz.getDeclaredConstructors()) {
            ctors.add(each);
            memberParameters.put(each, each.getParameterTypes());
        }
    }

    private void loadMethods() {
        for (Method each : clazz.getMethods()) {
            String methodName = each.getName();
            Class<?>[] paramTypes = each.getParameterTypes();
            List<Method> namedMethods = methodsByName.get(methodName);

            if (namedMethods == null) {
                namedMethods = new ArrayList<Method>();
                methodsByName.put(methodName, namedMethods);
            }

            if (!classIsAccessible(clazz))
                each = getAccessibleMethodFrom(clazz, methodName, paramTypes);

            if (each != null) {
                namedMethods.add(each);
                memberParameters.put(each, paramTypes);
            }
        }
    }

    private boolean memberIsMoreSpecific(Member first, Member second) {
        Class<?>[] firstParamTypes = memberParameters.get(first);
        Class<?>[] secondParamTypes = memberParameters.get(second);

        return compatibleClasses(secondParamTypes, firstParamTypes);
    }
}
